package com.share.device.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.share.common.core.context.SecurityContextHolder;
import com.share.common.core.domain.Result;
import com.share.common.core.exception.ServiceException;
import com.share.common.core.utils.bean.BeanUtils;
import com.share.device.domain.*;
import com.share.device.emqx.EmqxClientUtil;
import com.share.device.emqx.constants.EmqxConstants;
import com.share.device.service.*;
import com.share.order.api.RemoteOrderInfoService;
import com.share.order.domain.OrderInfo;
import com.share.rules.api.RemoteFeeRuleService;
import com.share.rules.domain.FeeRule;
import com.share.user.api.RemoteH5UserService;
import com.share.user.domain.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metric;
import org.springframework.data.geo.Metrics;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class DeviceServiceImpl implements IDeviceService {

    //mongoDB模版类 操作MongoDB
    @Autowired
    private MongoTemplate mongoTemplate;
    @Autowired
    private IStationService stationService;
    @Autowired
    private ICabinetService cabinetService;
    @Autowired
    private IMapService mapService;
    @Autowired
    private RemoteFeeRuleService remoteFeeRuleService;
    @Autowired
    private RemoteH5UserService remoteH5UserService;
    @Autowired
    private RemoteOrderInfoService remoteOrderInfoService;
    @Autowired
    private EmqxClientUtil emqxClientUtil;
    @Autowired
    private ICabinetSlotService cabinetSlotService;
    @Autowired
    private IPowerBankService powerBankService;

    //查询当前位置附近范围内的站点 参数纬度 经度
    @Override
    public List<StationVo> nearbyStation(String latitude, String longitude) {
        //1.根据经纬度获取到当前所在位置 mongoDB封装了一系列对于地图位置的查询操作
        //参数 经度 纬度
        GeoJsonPoint geoJsonPoint = new GeoJsonPoint(Double.parseDouble(longitude),
                Double.parseDouble(latitude));

        //2.创建距离对象 假设寻找50km范围内的站点 以此为半径即可  参数：距离值 单位（km）
        Distance distance = new Distance(10, Metrics.KILOMETERS);

        //3.创建查找的范围 以当前位置为圆心 距离为半径的圆
        Circle circle = new Circle(geoJsonPoint, distance);

        //4.通过MongoDB提供的方法在MongoDB中查询到附近站点
        //Query对象 查询的条件 where参数为在MongoDB中要查询的字段 withinSphere为查询的圆范围
        Query query = Query.query(Criteria.where("location").withinSphere(circle));

        List<StationLocation> stationLocationList = mongoTemplate.find(query, StationLocation.class);
        if (CollectionUtils.isEmpty(stationLocationList)) return null;

        //将数据封装为StationVo
        //获取所有的stationId
        List<Long> stationIdList = new ArrayList<>();
        for (StationLocation stationLocation : stationLocationList) {
            stationIdList.add(stationLocation.getStationId());
        }

        //根据stationID获取对应station 站点列表 select * from station where id  in ...
        LambdaQueryWrapper<Station> wrapper = new LambdaQueryWrapper();
        wrapper.in(Station::getId, stationIdList);
        List<Station> stationList = stationService.list(wrapper);

        //获取柜机id列表
        List<Long> cabinetIdList = stationList.stream().map(Station::getCabinetId).collect(Collectors.toList());

        //将柜机id和对应柜机对象封装到map中
        Map<Long, Cabinet> cabinetMap = new HashMap<>();
        for (Long id : cabinetIdList) {
            Cabinet cabinet = cabinetService.getById(id);
            cabinetMap.put(id, cabinet);
        }

        //将以上所有数据封装为stationVoList
        List<StationVo> stationVoList = new ArrayList<>();

        for (Station station : stationList) {
            //拷贝站点属性
            StationVo stationVo = new StationVo();
            BeanUtil.copyProperties(station, stationVo);

            //设置可用 可还 属性
            //查找当前站点对应的柜机
            Cabinet tempCabinet = cabinetMap.get(station.getCabinetId());

            //如果柜机中可用充电宝数量大于0 可借用
            if (tempCabinet.getAvailableNum() > 0) {
                stationVo.setIsUsable("1"); //1.可借
            } else stationVo.setIsUsable("0"); //0.不可借

            //如果柜机中空闲槽位数量大于0 可还
            if (tempCabinet.getFreeSlots() > 0) {
                stationVo.setIsReturn("1");
            } else stationVo.setIsReturn("0");

            //设置每个站点离用户起点位置的最短距离
            Double nearestDistance = mapService.calculateDistance(longitude, latitude,
                    station.getLongitude().toString(), station.getLatitude().toString());
            stationVo.setDistance(nearestDistance);

            //获取每个站点的计费规则
            Result<FeeRule> feeRuleResult = remoteFeeRuleService.getFeeRule(station.getFeeRuleId());
            FeeRule feeRule = feeRuleResult.getData();
            stationVo.setFeeRule(feeRule.getDescription());

            //将vo添加入list
            stationVoList.add(stationVo);
        }

        return stationVoList;
    }

    //根据id获取门店详情
    @Override
    public StationVo getStationDetail(Long id, String latitude, String longitude) {

        // 根据id获取门店详情 mp
        Station station = stationService.getById(id);

        //计算距离
        Double distance = mapService.calculateDistance(latitude, latitude,
                station.getLatitude().toString(), station.getLongitude().toString());

        //获取计费规则
        Result<FeeRule> feeRuleResult = remoteFeeRuleService.getFeeRule(id);
        FeeRule feeRule = feeRuleResult.getData();

        //将数据封装到VO中
        StationVo stationVo = new StationVo();
        BeanUtils.copyProperties(station, stationVo);

        stationVo.setFeeRule(feeRule.getDescription()); //费用规则
        stationVo.setDistance(distance);  //离用户距离

        //设置可借用 可归还状态数据
        //获取柜机信息
        Cabinet cabinet = cabinetService.getById(station.getCabinetId());

        //可借用
        if (cabinet.getAvailableNum() > 0) {
            stationVo.setIsUsable("1");
        } else {
            stationVo.setIsUsable("0");
        }

        //可归还
        if (cabinet.getFreeSlots() > 0) {
            stationVo.setIsReturn("1");
        } else {
            stationVo.setIsReturn("0");
        }

        return stationVo;
    }

    /**
     * 扫码充电后触发 接口
     * 传入路径参数 被扫描的柜机编号
     */
    @Transactional
    @Override
    public ScanChargeVo scanCharge(String cabinetNo) {

        //1.远程调用用户模块：根据用户id获得用户信息，判断是否支持免押金
        Result<UserInfo> userInfoResult = remoteH5UserService.getUserInfo(SecurityContextHolder.getUserId());
        UserInfo userInfo = userInfoResult.getData();

        if (userInfo == null) {
            throw new ServiceException("获取用户信息失败");
        }

        //未验证押金支付状态
        if (userInfo.getDepositStatus().equals("0")) {
            throw new ServiceException("未申请免押金使用");
        }

        ScanChargeVo scanChargeVo = new ScanChargeVo();

        //2.远程调用订单模块，判断用户是否有未完成订单 正在使用的充电宝或未付款订单
        Result<OrderInfo> noFinishOrder = remoteOrderInfoService.getNoFinishOrder(SecurityContextHolder.getUserId());
        OrderInfo orderInfo = noFinishOrder.getData();

        if (orderInfo != null) {
            //说明有未完成的订单
            String status = orderInfo.getStatus();
            if (status.equals("0")) {
                //有正在充电的订单，封装VO返回给前端
                scanChargeVo.setStatus("2"); //有未归还充电宝
                scanChargeVo.setMessage("有未归还充电宝，请归还后使用");
                return scanChargeVo;

            }
            if (status.equals("1")) {
                //有未支付的订单，封装VO返回给前端
                scanChargeVo.setStatus("2"); //有未支付订单
                scanChargeVo.setMessage("有未支付的订单，请先完成支付");
                return scanChargeVo;
            }
        }

        //3.从柜机里面选择最优的充电宝，获取其信息
        //封装方法，寻找指定柜机中电量最多的充电宝
        AvailableProwerBankVo availableProwerBankVo = this.checkAvailableProwerBank(cabinetNo);

        if (availableProwerBankVo == null) {
            throw new ServiceException("当前柜机无可用充电宝，请寻找其他柜机");
        }
        if (availableProwerBankVo.getErrMessage() != null) {
            //找到的充电宝有错误信息,输出错误信息
            throw new ServiceException(availableProwerBankVo.getErrMessage());
        }

        //4.发送MQTT消息，弹出对应充电宝
        //发送消息的topic 参数:常量topic模版 柜机编号替换占位符 %s  可以唯一标识一个柜机
        //    /sys/scan/submit/%s => /sys/scan/submit/....
        String topic = String.format(EmqxConstants.TOPIC_SCAN_SUBMIT, cabinetNo);

        //弹出充电宝的必要信息
        //封装消息对象
        JSONObject messageObject = new JSONObject();
        //存入需要的自定义数据
        messageObject.put("uId", SecurityContextHolder.getUserId()); //用户id
        messageObject.put("mNo", "mm" + RandomUtil.randomString(8)); //随机的消息编号 mm开头
        messageObject.put("cNo", cabinetNo);  //柜机编号
        messageObject.put("pNo", availableProwerBankVo.getPowerBankNo()); //弹出的充电宝编号
        messageObject.put("sNo", availableProwerBankVo.getSlotNo()); //弹出的充电宝的槽位编号

        String message = messageObject.toJSONString();
        emqxClientUtil.publish(topic, message);

        //5.封装VO数据返回给前端 设置VO对象 状态为1 ：成功
        scanChargeVo.setStatus("1");
        return scanChargeVo;
    }

    //查找指定柜机里最优的充电宝，也就是电量最多的充电宝
    private AvailableProwerBankVo checkAvailableProwerBank(String cabinetNo) {
        //1.创建AvailableProwerBankVo对象
        AvailableProwerBankVo availableProwerBankVo = new AvailableProwerBankVo();

        //2.根据cabinetNo查询柜机 mp
        LambdaQueryWrapper<Cabinet> cabinetLambdaQueryWrapper = new LambdaQueryWrapper<>();
        cabinetLambdaQueryWrapper.eq(Cabinet::getCabinetNo, cabinetNo);
        Cabinet cabinet = cabinetService.getOne(cabinetLambdaQueryWrapper);

        //3.判断柜机可用充电宝数量是否大于0
        if (cabinet.getAvailableNum() <= 0) {
            //无可用充电宝，设置错误信息返回数据
            availableProwerBankVo.setErrMessage("当前柜机无可用充电宝");
            return availableProwerBankVo;
        }

        //4.根据柜机id查询 所有插槽的List mp
        Long cabinetId = cabinet.getId();//柜机id

        LambdaQueryWrapper<CabinetSlot> slotWrapper = new LambdaQueryWrapper<CabinetSlot>();
        slotWrapper.eq(CabinetSlot::getCabinetId, cabinet.getId())
                .eq(CabinetSlot::getStatus, "1"); //状态 1：有充电宝 0：无充电宝 2：已锁定，避免其他用户扫码选中同一充电宝
        List<CabinetSlot> cabinetSlotList = cabinetSlotService.list(slotWrapper);

        //5.从插槽List获得对应的充电宝id的List
        List<Long> powerBankIdList = new ArrayList<>();
        for (CabinetSlot cabinetSlot : cabinetSlotList) {
            if (cabinetSlot.getPowerBankId() != null) {
                powerBankIdList.add(cabinetSlot.getPowerBankId());
            }
        }

        //6.根据充电宝id List查询充电宝List
        LambdaQueryWrapper<PowerBank> powerBankWrapper = new LambdaQueryWrapper<>();//充电宝状态：状态（0:未投放 1：可用 2：已租用 3：充电中 4：故障）
        powerBankWrapper.in(PowerBank::getId, powerBankIdList)
                .eq(PowerBank::getStatus, "1")
                .orderByDesc(PowerBank::getElectricity); //7.将充电宝List按照电量进行降序排序
        List<PowerBank> powerBankList = powerBankService.list(powerBankWrapper);

        if (powerBankList.isEmpty()) {
            //未查找到对应充电宝
            availableProwerBankVo.setErrMessage("无可用充电宝，请寻找其他柜机");
            return availableProwerBankVo;
        }

        //8.获得电量最多的充电宝的信息
        PowerBank bestPowerBank = powerBankList.get(0);

        //9.获得对应充电宝的槽位信息
        CabinetSlot bestSlot = cabinetSlotList.stream()
                .filter(item -> null != item.getPowerBankId()
                        && item.getPowerBankId().equals(bestPowerBank.getId())).collect(Collectors.toList()).get(0);

        //10.锁定该槽位 更新状态
        bestSlot.setStatus("2");
        cabinetSlotService.updateById(bestSlot);

        //11.封装VO数据返回
        availableProwerBankVo.setPowerBankNo(bestPowerBank.getPowerBankNo());//弹出充电宝编号
        availableProwerBankVo.setSlotNo(bestSlot.getSlotNo()); //充电宝所在插槽编号

        return availableProwerBankVo;
    }
}
